diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-15 12:52:11 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-15 12:52:11 +0000 |
| commit | b54f6f03150dd78d86db62201b6386bf14b72394 (patch) | |
| tree | b3092bb34805fdc65eee5282e86a9fb90ba20d6e /app/[lng]/partners/data-room/[projectId]/settings/page.tsx | |
| parent | c1bd1a2f499ee2f0742170021b37dab410983ab7 (diff) | |
(대표님) 커버, 데이터룸, 파일매니저, 담당자할당 등
Diffstat (limited to 'app/[lng]/partners/data-room/[projectId]/settings/page.tsx')
| -rw-r--r-- | app/[lng]/partners/data-room/[projectId]/settings/page.tsx | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/app/[lng]/partners/data-room/[projectId]/settings/page.tsx b/app/[lng]/partners/data-room/[projectId]/settings/page.tsx new file mode 100644 index 00000000..aa0f3b52 --- /dev/null +++ b/app/[lng]/partners/data-room/[projectId]/settings/page.tsx @@ -0,0 +1,488 @@ + +// app/projects/[projectId]/settings/page.tsx +'use client'; + +import { useState, useEffect } from 'react'; +import { + Settings, + Shield, + Globe, + Trash2, + AlertCircle, + Save, + Lock, + Unlock, + Archive, + Users, + HardDrive +} from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useToast } from '@/hooks/use-toast'; +import { useRouter } from 'next/navigation'; + +interface ProjectSettings { + id: string; + name: string; + description: string; + isPublic: boolean; + externalAccessEnabled: boolean; + storageLimit: number; + maxFileSize: number; + allowedFileTypes: string[]; + autoArchiveDays: number; + requireApproval: boolean; + defaultCategory: string; +} + +export default function ProjectSettingsPage({ + params +}: { + params: { projectId: string } +}) { + const [settings, setSettings] = useState<ProjectSettings | null>(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [archiveDialogOpen, setArchiveDialogOpen] = useState(false); + const [currentUserRole, setCurrentUserRole] = useState<string>('viewer'); + + const { toast } = useToast(); + const router = useRouter(); + + useEffect(() => { + fetchSettings(); + checkUserRole(); + }, [params.projectId]); + + const fetchSettings = async () => { + try { + setLoading(true); + const response = await fetch(`/api/projects/${params.projectId}/settings`); + + if (!response.ok) { + throw new Error('설정을 불러올 수 없습니다'); + } + + const data = await response.json(); + setSettings(data); + } catch (error) { + toast({ + title: '오류', + description: '프로젝트 설정을 불러올 수 없습니다.', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + const checkUserRole = async () => { + try { + const response = await fetch(`/api/projects/${params.projectId}/access`); + const data = await response.json(); + setCurrentUserRole(data.role); + } catch (error) { + console.error('권한 확인 실패:', error); + } + }; + + const saveSettings = async () => { + if (!settings) return; + + try { + setSaving(true); + const response = await fetch(`/api/projects/${params.projectId}/settings`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings), + }); + + if (!response.ok) throw new Error('설정 저장 실패'); + + toast({ + title: '성공', + description: '프로젝트 설정이 저장되었습니다.', + }); + } catch (error) { + toast({ + title: '오류', + description: '설정 저장에 실패했습니다.', + variant: 'destructive', + }); + } finally { + setSaving(false); + } + }; + + const deleteProject = async () => { + try { + const response = await fetch(`/api/projects/${params.projectId}`, { + method: 'DELETE', + }); + + if (!response.ok) throw new Error('프로젝트 삭제 실패'); + + toast({ + title: '성공', + description: '프로젝트가 삭제되었습니다.', + }); + + router.push('/projects'); + } catch (error) { + toast({ + title: '오류', + description: '프로젝트 삭제에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + const archiveProject = async () => { + try { + const response = await fetch(`/api/projects/${params.projectId}/archive`, { + method: 'POST', + }); + + if (!response.ok) throw new Error('프로젝트 보관 실패'); + + toast({ + title: '성공', + description: '프로젝트가 보관되었습니다.', + }); + + router.push('/projects'); + } catch (error) { + toast({ + title: '오류', + description: '프로젝트 보관에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + const canEdit = currentUserRole === 'owner' || currentUserRole === 'admin'; + + if (loading || !settings) { + return ( + <div className="p-6"> + <div className="animate-pulse space-y-4"> + {[...Array(5)].map((_, i) => ( + <div key={i} className="h-20 bg-gray-200 rounded" /> + ))} + </div> + </div> + ); + } + + return ( + <div className="p-6 space-y-6 max-w-4xl"> + {/* 헤더 */} + <div className="flex items-center justify-between"> + <div> + <h1 className="text-2xl font-bold">프로젝트 설정</h1> + <p className="text-muted-foreground mt-1"> + 프로젝트 설정을 관리합니다 + </p> + </div> + + {canEdit && ( + <Button onClick={saveSettings} disabled={saving}> + <Save className="h-4 w-4 mr-2" /> + {saving ? '저장 중...' : '변경사항 저장'} + </Button> + )} + </div> + + {!canEdit && ( + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + 프로젝트 설정을 변경하려면 Owner 또는 Admin 권한이 필요합니다. + </AlertDescription> + </Alert> + )} + + <Tabs defaultValue="general"> + <TabsList> + <TabsTrigger value="general">일반</TabsTrigger> + <TabsTrigger value="access">접근 관리</TabsTrigger> + <TabsTrigger value="storage">스토리지</TabsTrigger> + {currentUserRole === 'owner' && ( + <TabsTrigger value="danger">위험 영역</TabsTrigger> + )} + </TabsList> + + <TabsContent value="general" className="space-y-4"> + <Card> + <CardHeader> + <CardTitle>기본 정보</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div> + <Label htmlFor="name">프로젝트 이름</Label> + <Input + id="name" + value={settings.name} + onChange={(e) => setSettings({ ...settings, name: e.target.value })} + disabled={!canEdit} + /> + </div> + + <div> + <Label htmlFor="description">설명</Label> + <Textarea + id="description" + value={settings.description} + onChange={(e) => setSettings({ ...settings, description: e.target.value })} + disabled={!canEdit} + rows={3} + /> + </div> + + <div> + <Label htmlFor="category">기본 파일 카테고리</Label> + <Select + value={settings.defaultCategory} + onValueChange={(value) => setSettings({ ...settings, defaultCategory: value })} + disabled={!canEdit} + > + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="public">Public - 공개</SelectItem> + <SelectItem value="restricted">Restricted - 제한</SelectItem> + <SelectItem value="confidential">Confidential - 기밀</SelectItem> + <SelectItem value="internal">Internal - 내부</SelectItem> + </SelectContent> + </Select> + </div> + </CardContent> + </Card> + </TabsContent> + + <TabsContent value="access" className="space-y-4"> + <Card> + <CardHeader> + <CardTitle>접근 설정</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div className="flex items-center justify-between"> + <div> + <Label htmlFor="public">공개 프로젝트</Label> + <p className="text-sm text-muted-foreground"> + 모든 사용자가 이 프로젝트를 볼 수 있습니다 + </p> + </div> + <Switch + id="public" + checked={settings.isPublic} + onCheckedChange={(checked) => setSettings({ ...settings, isPublic: checked })} + disabled={!canEdit} + /> + </div> + + <div className="flex items-center justify-between"> + <div> + <Label htmlFor="external">외부 사용자 접근 허용</Label> + <p className="text-sm text-muted-foreground"> + 파트너사 사용자도 접근할 수 있습니다 + </p> + </div> + <Switch + id="external" + checked={settings.externalAccessEnabled} + onCheckedChange={(checked) => + setSettings({ ...settings, externalAccessEnabled: checked }) + } + disabled={!canEdit} + /> + </div> + + <div className="flex items-center justify-between"> + <div> + <Label htmlFor="approval">멤버 승인 필요</Label> + <p className="text-sm text-muted-foreground"> + 새 멤버 참여 시 관리자 승인이 필요합니다 + </p> + </div> + <Switch + id="approval" + checked={settings.requireApproval} + onCheckedChange={(checked) => + setSettings({ ...settings, requireApproval: checked }) + } + disabled={!canEdit} + /> + </div> + </CardContent> + </Card> + </TabsContent> + + <TabsContent value="storage" className="space-y-4"> + <Card> + <CardHeader> + <CardTitle>스토리지 설정</CardTitle> + </CardHeader> + <CardContent className="space-y-4"> + <div> + <Label htmlFor="storage-limit">스토리지 제한 (GB)</Label> + <Input + id="storage-limit" + type="number" + value={settings.storageLimit} + onChange={(e) => setSettings({ + ...settings, + storageLimit: parseInt(e.target.value) + })} + disabled={!canEdit} + /> + </div> + + <div> + <Label htmlFor="file-size">최대 파일 크기 (MB)</Label> + <Input + id="file-size" + type="number" + value={settings.maxFileSize} + onChange={(e) => setSettings({ + ...settings, + maxFileSize: parseInt(e.target.value) + })} + disabled={!canEdit} + /> + </div> + + <div> + <Label htmlFor="auto-archive">자동 보관 (일)</Label> + <Input + id="auto-archive" + type="number" + value={settings.autoArchiveDays} + onChange={(e) => setSettings({ + ...settings, + autoArchiveDays: parseInt(e.target.value) + })} + disabled={!canEdit} + /> + <p className="text-sm text-muted-foreground mt-1"> + 설정한 기간 동안 접근하지 않은 파일을 자동으로 보관합니다 + </p> + </div> + </CardContent> + </Card> + </TabsContent> + + {currentUserRole === 'owner' && ( + <TabsContent value="danger" className="space-y-4"> + <Card className="border-red-200"> + <CardHeader> + <CardTitle className="text-red-600">위험 영역</CardTitle> + <CardDescription> + 이 작업들은 되돌릴 수 없습니다. 신중하게 진행하세요. + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="flex items-center justify-between p-4 border rounded-lg"> + <div> + <h3 className="font-medium">프로젝트 보관</h3> + <p className="text-sm text-muted-foreground"> + 프로젝트를 보관하면 읽기 전용이 됩니다 + </p> + </div> + <Button + variant="outline" + onClick={() => setArchiveDialogOpen(true)} + > + <Archive className="h-4 w-4 mr-2" /> + 프로젝트 보관 + </Button> + </div> + + <div className="flex items-center justify-between p-4 border rounded-lg border-red-200"> + <div> + <h3 className="font-medium text-red-600">프로젝트 삭제</h3> + <p className="text-sm text-muted-foreground"> + 프로젝트와 모든 파일을 영구적으로 삭제합니다 + </p> + </div> + <Button + variant="destructive" + onClick={() => setDeleteDialogOpen(true)} + > + <Trash2 className="h-4 w-4 mr-2" /> + 프로젝트 삭제 + </Button> + </div> + </CardContent> + </Card> + </TabsContent> + )} + </Tabs> + + {/* 삭제 확인 다이얼로그 */} + <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>프로젝트 삭제</DialogTitle> + <DialogDescription className="text-red-600"> + 정말로 이 프로젝트를 삭제하시겠습니까? + 모든 파일과 데이터가 영구적으로 삭제됩니다. + </DialogDescription> + </DialogHeader> + <DialogFooter> + <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}> + 취소 + </Button> + <Button variant="destructive" onClick={deleteProject}> + 삭제 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 보관 확인 다이얼로그 */} + <Dialog open={archiveDialogOpen} onOpenChange={setArchiveDialogOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>프로젝트 보관</DialogTitle> + <DialogDescription> + 프로젝트를 보관하시겠습니까? + 보관된 프로젝트는 읽기 전용이 되며, 언제든지 복원할 수 있습니다. + </DialogDescription> + </DialogHeader> + <DialogFooter> + <Button variant="outline" onClick={() => setArchiveDialogOpen(false)}> + 취소 + </Button> + <Button onClick={archiveProject}> + 보관 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + </div> + ); +} |
